Análise de Dados preliminar com Python & Jupyter Notebooks¶

Case - Cancelamento de Clientes¶

Uma empresa hipotética com mais de 800 mil clientes deseja investigar por que a maioria de seus clientes registrados na base total são inativos (ou seja, cancelaram o serviço).

Essa análise de dados tem intuito de identificar os principais motivos desses cancelamentos e quais as ações mais eficientes para reduzir esse número.

Base de dados: cancelamentos.csv

In [ ]:
### Magic commands 
# In Jupyter Notebooks Online use %pip install
# In Vscode or other Python environments use !pip install

# É possível fazer a instalação de pacotes diretamente nos notebooks, como se fosse um terminal. Basta clicar no botão e rodar.
# !pip install pandas   ---> Biblioteca bastante usada para manipular base de dados.
# !pip install numpy    ---> Fornece suporte para matrizes e arrays grandes e multidimensionais, juntamente com uma coleção de funções matemáticas para operar nessas matrizes.
# !pip install openpyxl ---> Uma biblioteca Python para leitura e gravação de arquivos Excel (XLSX).
# !pip install plotly   ---> Possibilita a criação de gráficos para a visualização de dados interativos e dinâmicos. É uma biblioteca conhecida por sua facilidade de uso.

# HTML export (with graphs)
# import plotly.io as pio
# pio.renderers.default = 'notebook'

# PDF export (with graphs)
# !pip install Pyppeteer
# !pyppeteer-install
# %pip install --upgrade plotly
# %pip install -q --upgrade nbformat
# %pip install --upgrade notebook

# import plotly
# print(plotly.__version__) # OUTPUT: 5.17.0
In [ ]:
# ETAPA 1: Importar e visualizar a base de dados
import pandas as pd

tabela = pd.read_csv("cancelamentos.csv")
# "Informação que não ajuda, atrapalha". É importante remover a coluna dos IDs dos usuários, visto que não ajuda na análise
tabela = tabela.drop(columns="CustomerID")

# Visualizar a base de dados
print("Visualização da base de dados (sem modificações): ")
display(tabela) # Função especial do Jupyte Notebooks
Visualização da base de dados (sem modificações): 
idade sexo tempo_como_cliente frequencia_uso ligacoes_callcenter dias_atraso assinatura duracao_contrato total_gasto meses_ultima_interacao cancelou
0 30.0 Female 39.0 14.0 5.0 18.0 Standard Annual 932.00 17.0 1.0
1 65.0 Female 49.0 1.0 10.0 8.0 Basic Monthly 557.00 6.0 1.0
2 55.0 Female 14.0 4.0 6.0 18.0 Basic Quarterly 185.00 3.0 1.0
3 58.0 Male 38.0 21.0 7.0 7.0 Standard Monthly 396.00 29.0 1.0
4 23.0 Male 32.0 20.0 5.0 8.0 Basic Monthly 617.00 20.0 1.0
... ... ... ... ... ... ... ... ... ... ... ...
881661 42.0 Male 54.0 15.0 1.0 3.0 Premium Annual 716.38 8.0 0.0
881662 25.0 Female 8.0 13.0 1.0 20.0 Premium Annual 745.38 2.0 0.0
881663 26.0 Male 35.0 27.0 1.0 5.0 Standard Quarterly 977.31 9.0 0.0
881664 28.0 Male 55.0 14.0 2.0 0.0 Standard Quarterly 602.55 2.0 0.0
881665 31.0 Male 48.0 20.0 1.0 14.0 Premium Quarterly 567.77 21.0 0.0

881666 rows × 11 columns

In [ ]:
# ETAPA 2: Tratar problemas da base de dados

# Com o método .info() é possível encontrar valores vazios e valores do tipo errado na tabela
display(tabela.info()) 

# Joga fora todas as linhas que tem algum valor vazio
tabela = tabela.dropna() 

# Agora a tabela foi adequadamente tratada e está pronta p/ ser analisada
display(tabela.info()) 
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 881666 entries, 0 to 881665
Data columns (total 11 columns):
 #   Column                  Non-Null Count   Dtype  
---  ------                  --------------   -----  
 0   idade                   881664 non-null  float64
 1   sexo                    881664 non-null  object 
 2   tempo_como_cliente      881663 non-null  float64
 3   frequencia_uso          881663 non-null  float64
 4   ligacoes_callcenter     881664 non-null  float64
 5   dias_atraso             881664 non-null  float64
 6   assinatura              881661 non-null  object 
 7   duracao_contrato        881663 non-null  object 
 8   total_gasto             881664 non-null  float64
 9   meses_ultima_interacao  881664 non-null  float64
 10  cancelou                881664 non-null  float64
dtypes: float64(8), object(3)
memory usage: 74.0+ MB
None
<class 'pandas.core.frame.DataFrame'>
Index: 881659 entries, 0 to 881665
Data columns (total 11 columns):
 #   Column                  Non-Null Count   Dtype  
---  ------                  --------------   -----  
 0   idade                   881659 non-null  float64
 1   sexo                    881659 non-null  object 
 2   tempo_como_cliente      881659 non-null  float64
 3   frequencia_uso          881659 non-null  float64
 4   ligacoes_callcenter     881659 non-null  float64
 5   dias_atraso             881659 non-null  float64
 6   assinatura              881659 non-null  object 
 7   duracao_contrato        881659 non-null  object 
 8   total_gasto             881659 non-null  float64
 9   meses_ultima_interacao  881659 non-null  float64
 10  cancelou                881659 non-null  float64
dtypes: float64(8), object(3)
memory usage: 80.7+ MB
None
In [ ]:
# ETAPA 3: Primeira verificação -> Descobrir o percentual de clientes que cancelou

# Na coluna "cancelou" da tabela, value_counts() agrupa valores em categoria (int64)
display( tabela["cancelou"].value_counts() )

# Com o parâmetro 'normalize' verdadeiro, a função retorna em porcentagem (float64)
# Normalizar: indice_total / indice_de_valor_especifico 
# Logo, 499993 / 881659 --> para quem cancelou & 381666 / 881659 para quem não cancelou
display( tabela["cancelou"].value_counts(normalize=True) ) 

# EXTRA: Visualizando em porcentuual ao invés de float (usando o método .map() do Python)
#  .map("{:.1%}".format) --> Códigos de formatação são escritos como string "{:.1%}"
# "{:.1%}" --> Significa: Mostre o resultado como percentual e com apenas uma casa depois da vírgula
# OBS: É melhor não usar pois transforma o tipo de dado em objeto, o que não permite realizar cálculos


# CONCLUSÃO: Mais de 50% dos clientes cancelaram
cancelou
1.0    499993
0.0    381666
Name: count, dtype: int64
cancelou
1.0    0.567105
0.0    0.432895
Name: proportion, dtype: float64

Percentual de clientes que cancelaram suas assinaturas: 56%¶

In [ ]:
# Que tipo de contrato (mensal, trimestral, anual) tende a ser mais cancelado?
display( tabela["duracao_contrato"].value_counts() ) 
display( tabela["duracao_contrato"].value_counts(normalize=True).map("{:.1%}".format) ) 
duracao_contrato
Annual       354395
Quarterly    353059
Monthly      174205
Name: count, dtype: int64
duracao_contrato
Annual       40.2%
Quarterly    40.0%
Monthly      19.8%
Name: proportion, dtype: object
In [ ]:
# Agrupamento (uma tabelinha já com os dados calculados de cada categoria que ajuda com a análise)

# Fazendo a média de cada coluna numérica
tabela_agrupada = tabela.groupby("duracao_contrato").mean(numeric_only=True)

display(tabela_agrupada)
idade tempo_como_cliente frequencia_uso ligacoes_callcenter dias_atraso total_gasto meses_ultima_interacao cancelou
duracao_contrato
Annual 38.842165 31.446186 15.880213 3.263401 12.465156 651.697738 14.236107 0.460760
Monthly 41.552407 30.538555 15.499274 4.985649 15.007267 550.616435 15.478012 1.000000
Quarterly 38.830938 31.419916 15.886662 3.265245 12.460863 651.427783 14.234544 0.460255

O tipo de assinatura mais cancelada é a mensal (100% de cancelamento)¶

CONCLUSÃO: Todos os clientes do contrato mensal cancelaram.

CAUSA: Provavelmente porque o plano não oferece benefícios satisfatórios.

SUGESTÃO: Oferecer desconto nos contratos anuais/trimestrais (pois eles são melhores). Ou melhorar os benefícios do contrato mensal.

In [ ]:
# Agora que já se sabe porque as pessoas do plano mensal cancelaram, é hora de excluí-las da tabela para analisar outras causas de cancelamento adjacentes
# Excluindo a coluna do contrato mensal e armazenando em uma nova variável (tabela_filtrada)

# A variável exclui_mensal conterá um valor booleano (True ou False) para cada linha da coluna "duracao_contrato" da tabela
exclui_mensal = tabela["duracao_contrato"] != "Monthly"

# "True" : Não são do plano mensal 
# "False" : São do plano mensal
display(exclui_mensal) 
0          True
1         False
2          True
3         False
4         False
          ...  
881661     True
881662     True
881663     True
881664     True
881665     True
Name: duracao_contrato, Length: 881659, dtype: bool
In [ ]:
# Comparando tabelas

# A tabela resultante só vai mostrar as linhas que retornarem "True" na operação realizada pela variável exclui_mensal 
tabela_filtrada = tabela[exclui_mensal] # Com o pandas é possível passar uma condição dentro dos colchetes
#OBS: Aqui não seria possível usar o método .drop(), pois ele só funciona com colunas e "Monthly" é uma categoria presente em linhas

print("Tabela normal (com Monthly):")
display(tabela)

print("Tabela filtrada (sem Monthly):")
display(tabela_filtrada) # A tabela mostrada excluiu todas as linhas com contrato mensal
Tabela normal (com Monthly):
idade sexo tempo_como_cliente frequencia_uso ligacoes_callcenter dias_atraso assinatura duracao_contrato total_gasto meses_ultima_interacao cancelou
0 30.0 Female 39.0 14.0 5.0 18.0 Standard Annual 932.00 17.0 1.0
1 65.0 Female 49.0 1.0 10.0 8.0 Basic Monthly 557.00 6.0 1.0
2 55.0 Female 14.0 4.0 6.0 18.0 Basic Quarterly 185.00 3.0 1.0
3 58.0 Male 38.0 21.0 7.0 7.0 Standard Monthly 396.00 29.0 1.0
4 23.0 Male 32.0 20.0 5.0 8.0 Basic Monthly 617.00 20.0 1.0
... ... ... ... ... ... ... ... ... ... ... ...
881661 42.0 Male 54.0 15.0 1.0 3.0 Premium Annual 716.38 8.0 0.0
881662 25.0 Female 8.0 13.0 1.0 20.0 Premium Annual 745.38 2.0 0.0
881663 26.0 Male 35.0 27.0 1.0 5.0 Standard Quarterly 977.31 9.0 0.0
881664 28.0 Male 55.0 14.0 2.0 0.0 Standard Quarterly 602.55 2.0 0.0
881665 31.0 Male 48.0 20.0 1.0 14.0 Premium Quarterly 567.77 21.0 0.0

881659 rows × 11 columns

Tabela filtrada (sem Monthly):
idade sexo tempo_como_cliente frequencia_uso ligacoes_callcenter dias_atraso assinatura duracao_contrato total_gasto meses_ultima_interacao cancelou
0 30.0 Female 39.0 14.0 5.0 18.0 Standard Annual 932.00 17.0 1.0
2 55.0 Female 14.0 4.0 6.0 18.0 Basic Quarterly 185.00 3.0 1.0
5 51.0 Male 33.0 25.0 9.0 26.0 Premium Annual 129.00 8.0 1.0
6 58.0 Female 49.0 12.0 3.0 16.0 Standard Quarterly 821.00 24.0 1.0
7 55.0 Female 37.0 8.0 4.0 15.0 Premium Annual 445.00 30.0 1.0
... ... ... ... ... ... ... ... ... ... ... ...
881661 42.0 Male 54.0 15.0 1.0 3.0 Premium Annual 716.38 8.0 0.0
881662 25.0 Female 8.0 13.0 1.0 20.0 Premium Annual 745.38 2.0 0.0
881663 26.0 Male 35.0 27.0 1.0 5.0 Standard Quarterly 977.31 9.0 0.0
881664 28.0 Male 55.0 14.0 2.0 0.0 Standard Quarterly 602.55 2.0 0.0
881665 31.0 Male 48.0 20.0 1.0 14.0 Premium Quarterly 567.77 21.0 0.0

707454 rows × 11 columns

In [ ]:
# Comparando os cancelamentos
 
# Antes (56% Cancelados ---> Com Monthly incluso)
display( tabela["cancelou"].value_counts(normalize=True) ) 

# Depois (46% Cancelados ---> Sem Monthly incluso)
display( tabela_filtrada["cancelou"].value_counts(normalize=True) ) 
cancelou
1.0    0.567105
0.0    0.432895
Name: proportion, dtype: float64
cancelou
0.0    0.539492
1.0    0.460508
Name: proportion, dtype: float64
In [ ]:
import plotly.io as pio

# If this returns 'plotly_mimetype+notebook', comment the line below and run this block again
pio.renderers.default = 'plotly_mimetype'

# pio.renderers.default = 'notebook' # Uncomment this to allow HTML export

pio.renderers.default # Prints the default renderer
In [ ]:
# ETAPA 4: Visualizando a causa dos cancelamentos com a ajuda de gráficos

# ------------- Rode esse bloco de código para gerar os gráficos ------------- #

# Criar gráficos para fazer a análise com o a ferramenta express da biblioteca plotly
import plotly.express as px

# Ver os diferentes tipos de gráficos oferecidos pelo plotly: https://plotly.com/python/
# O gráfico que será usado aqui é o histograma

# Criando um sample de DataFrame p/ passar as cores
data = pd.DataFrame({
    'Category': ['A', 'B', 'C', 'A', 'B', 'C'],
    'Value': [1, 2, 3, 4, 5, 6]
})

# Definindo um color map customizável
color_map = {
    'A': 'red',
    'B': 'green',
    'C': 'blue'
}

for coluna in tabela.columns:
    # Criar o gráfico
    grafico = px.histogram(tabela_filtrada, x=coluna, color="cancelou", color_discrete_map=color_map, text_auto=True)

    # Exibir o gráfico
    grafico.show()

Após a análise do gráfico, percebeu-se 3 principais causas de cancelamento.¶

1- Assinatura mensal¶

Todos os clientes do contrato mensal cancelaram. Provavelmente porque o plano não oferece benefícios satisfatórios.

2- Quantidade de ligações feitas para o Call Center¶

Acima de 4 ligações feitas para o call center, o cliente tende a cancelar. Sendo que acima de 6 ligações, todos os clientes cancelaram.

3- Dias de atraso no pagamento da fatura¶

Acima de 20 dias de atraso, todos os clientes cancelaram.

In [ ]:
# Novamente, é hora de eliminar os casos em que já se obteve conclusões acerca do cancelamento
condicao_callcenter = tabela_filtrada["ligacoes_callcenter"] <= 4

# Ficando agora com a tabela que só inclui aquelas resultados que não ligaram pro call center mais de 4 vezes
tabela_filtrada = tabela_filtrada[condicao_callcenter]

# Mesma coisa, só que com os dias de atraso agora
condicao_atraso = tabela_filtrada["dias_atraso"] <= 20
tabela_filtrada = tabela_filtrada[condicao_atraso]

display( tabela_filtrada["cancelou"].value_counts(normalize=True) )
cancelou
0.0    0.816037
1.0    0.183963
Name: proportion, dtype: float64

Conclusões da análise¶

De acordo com a simulação acima, retiradas as três principais causas, a média de cancelamentos cairia para apenas 26% (comparado com a taxa inicial de 53%).

Agora sim a taxa de cancelamentos está muito mais saudável e dentro do esperado.